Skip to content

feat(cli): tell CLI/TUI when a newer release is available (#60) - #61

Merged
TriDefender merged 2 commits into
TriDefender:masterfrom
Ma6302:update-notice
Oct 2, 2026
Merged

TriDefender merged 2 commits into
TriDefender:masterfrom
Ma6302:update-notice

Conversation

@Ma6302

@Ma6302 Ma6302 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Refs #60 — notify-only scope.

What

Android already ships an UpdateChecker; the Linux / CLI / TUI side had no way to learn that a newer release exists. This ports the same behaviour, with no new dependency and no new port:

  • src/update/check.ts asks the GitHub latest release endpoint once, asynchronously at startup.
  • serve prints at most one extra update: line when a newer release exists; the TUI prints the same line and adds u for a manual re-check.
  • ZCODE_UPDATE_CHECK=off disables the check; ZCODE_UPDATE_SKIP=v4.7.6 mutes a single tag.
  • Inside a container the hint is docker compose pull && docker compose up -d (the image is immutable) — nothing is downloaded or replaced in place.

Behaviour contract (same as the Android checker)

  • Never throws: DNS/TLS/timeout, 403/404/429/5xx, non-JSON bodies, or a release whose tag_name is not a plain version all end in "no notice".
  • Never blocks startup: the check is fire-and-forget with a 10 s AbortController timeout whose timer is unref'd, so it cannot hold the process open.
  • Comparison is numeric on the first three segments (4.7.10 > 4.7.9); an unparsable tag is never reported as newer. Suffixed tags such as v4.7.2.android compare on the numeric core.
  • A manual check ignores both the env switch and the skip list and always answers (up to date / skipped / unavailable) instead of staying silent.
  • Requests carry a User-Agent (GitHub returns 403 without one) and Accept: application/vnd.github+json.

Why notify and not self-update

Release artifacts carry no checksum file, so a downloaded binary cannot be verified before replacing a running one; and inside Docker the image is immutable anyway. Android only notifies today, so this keeps one behaviour across platforms.

Tests

  • src/update/check.test.ts: 24 cases, all with an injected fetch (no network), covering version comparison, env/skip handling, container detection, notice text, response parsing and 8 failure modes.
  • bun x tsc --noEmit clean; full bun test 969 pass, plus the pre-existing Windows-only captcha-worker failure (an unloadable worker entry degrades to in-process solving) that also fails on master.
  • Live check against the real API: 4.7.4 → notice for v4.7.5; 4.7.5 → up to date; ZCODE_UPDATE_CHECK=off → unavailable.

Docs

README.md / README_EN.md: two environment rows plus an "update notice" paragraph.

Out of scope

Automatic download/replace, a checksum file, and a way to skip a version other than the env var. Happy to follow up on any of them.

Android 早就有 UpdateChecker,Linux/CLI/TUI 一直没有「有新版」提示,
这里把同一套做法移植过来:不加依赖、不开新端口。

- 启动时异步查一次 GitHub latest release,有新版最多多打一行 update: 日志;
  TUI 里按 u 可手动重查。
- 任何失败(离线、被挡、403/429/5xx、返回体不可解析)都静默忽略,
  从不抛异常、不阻塞启动;10s 超时用的定时器 unref,不吊住进程。
- ZCODE_UPDATE_CHECK=off 关闭检查;ZCODE_UPDATE_SKIP=v4.7.6 忽略某个版本。
- 容器内提示 docker compose pull && docker compose up -d(镜像不可变,
  release 也还没有校验和),不做自动下载替换。
- 手动检查不受环境变量与忽略列表影响,且一定给答复(已是最新 / 不可用)。

测试:src/update/check.test.ts 24 个用例(注入 fetch、不发网络请求);
bun x tsc --noEmit 通过;全量 bun test 969 通过(另有 Windows 既有的
captcha worker 用例失败,master 上同样失败)。

Refs TriDefender#60

Signed-off-by: Ma6302 <143102004+Ma6302@users.noreply.github.com>

@TriDefender TriDefender left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结合 issue #60 审查。启动异步检查、静默失败、环境变量开关、手动入口和不自动替换的总体方向符合需求。请修正下面几处行为问题后再合并。没有在本次审查中复跑测试。

Comment thread src/tui/app.ts Outdated
// ZCODE_UPDATE_CHECK=off and the muted-tag list, and always answers visibly.
let updateCheckInFlight = false;
async function runUpdateCheck(manual = false): Promise<void> {
if (updateCheckInFlight) return;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 启动自动检查还在进行时按 u 会静默失效。runTui 启动时已调用 runUpdateCheck(),这里会直接丢弃随后的手动请求;若启动检查最后是 up-to-date、skipped 或 unavailable,用户按 u 不会收到结果,也无法通过手动检查绕过 skip/关闭设置。请排队执行手动检查,或确保当前请求完成后再给出手动检查结果。

Comment thread src/update/check.ts Outdated

/** `v4.7.6-android` → `4.7.6`; `""` when there is no numeric core. */
function versionCore(tag: string): string {
const match = /^v?(\d+(?:\.\d+){0,2})/.exec(tag.trim());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 这个正则只匹配 tag 开头,没有校验数字版本后的边界。比如 v4.7.6garbage 会解析成 4.7.6,当当前版本是 4.7.5 时仍会持续报告有更新,与 PR 所述“非 plain version 不提示”不符。请只接受完整版本或明确允许的后缀形式,并覆盖这种畸形 tag。

Comment thread src/update/check.ts Outdated
}

export function updateCommand(release: ReleaseInfo, inContainer: boolean): string {
return inContainer ? "docker compose pull && docker compose up -d" : `re-download from ${release.url}`;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] isContainerRuntime 也会识别 Podman(/.containerenv),但这里始终输出 docker compose ...。Podman-only 主机收到的更新指令可能无法执行。请根据运行时生成对应命令,或改成不假定 Docker Compose 的通用容器更新提示。

- 版本 tag 只接受完整版本或明确分隔的变体后缀(v4.7.5 / 4.8 / v4.7.2.android /
  v4.5.4-AppOverhaul / v4.7.6-rc.1)。此前 `v4.7.6garbage`、`v4.7.6.1` 会被截成
  4.7.6,导致当前版本 4.7.5 时永久提示有新版;现在这类 tag 没有版本核心,
  一律不判为更新(补充畸形 tag 用例)。
- 容器提示按检测到的运行时生成,不再假定 Docker:Docker → docker compose pull &&
  docker compose up -d;Podman(/.containerenv 或 container=podman)→ podman compose
  pull && podman compose up -d;识别不出的容器(如 container=lxc)→ 不假定 compose 的
  通用提示「pull the new image and recreate the container」。isContainerRuntime 保留为
  detectContainerRuntime 的兼容包装。
- TUI:启动时的自动检查还在进行中按 u 不再被静默丢弃。新增 createUpdateCheckQueue
  单飞调度:自动检查重复调用直接丢弃,手动检查排队并在当前请求结束后用 force 重跑
  (启动那次可能是 up-to-date / skipped / unavailable,或被 ZCODE_UPDATE_CHECK=off
  关掉,不能当作手动请求的答复),重复按 u 合并为一次。
- README / README_EN 同步说明容器提示与运行时有关。

Refs TriDefender#60
Signed-off-by: Ma6302 <143102004+Ma6302@users.noreply.github.com>
@Ma6302

Ma6302 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

谢谢审查。三处都改了,第二提交 c31f10d7。逐条对应:

1. src/tui/app.ts:246(P2)启动检查进行中按 u 被丢弃

已改为排队。新增 createUpdateCheckQueue(run) 单飞调度:请求在飞时,自动调用直接丢弃(启动那次已经覆盖),手动调用置 manualQueued,当前请求 settle 后用 force 重跑一次;连按 u 合并成一次。

这里确实不能把启动那次的结果当手动答复:它可能是 up-to-date / skipped / unavailable,也可能被 ZCODE_UPDATE_CHECK=off 关掉,任何一种都不代表「用户按了 u 之后的最新结论」。

2. src/update/check.ts:100(P2)畸形 tag 被截成版本号

版本 tag 改为严格语法,数字版本后必须是字符串结尾或明确分隔符 + 字母开头的后缀:

const VERSION_TAG = /^v?(\d+(?:\.\d+){0,2})(?:[.-][A-Za-z][0-9A-Za-z.-]*)?$/;

于是 v4.7.6garbage、v4.7.6.1、v4.7.6-、Windows 都没有版本核心 → 永不判为更新;仓库里真实存在的 v4.7.2.android、v4.5.4-AppOverhaul、v1.4.7.alpha 仍按数字核心比较(我先 gh api .../tags 核对过这些 tag,所以没有一刀切去掉后缀支持)。parseVersion / isNewerVersion / isSkippedVersion 共用一个 versionCore(),跳过列表里的畸形值同样不再匹配到任何版本。畸形 tag 用例已补进单测(24 → 33 个用例)。

3. src/update/check.ts:137(P3)Podman 也提示 docker compose

新增 detectContainerRuntime(env, exists): "docker" | "podman" | "unknown" | null(/.dockerenv → docker,/run/.containerenv 或 container=podman → podman,其他非空 container → unknown,都没有 → null),提示按检测结果生成:

  • docker → docker compose pull && docker compose up -d
  • podman → podman compose pull && podman compose up -d
  • unknown(例如 container=lxc)→ pull the new image and recreate the container(不假定 compose)
  • 非容器 → re-download from <release url>

isContainerRuntime 保留为 detectContainerRuntime(...) !== null 的兼容包装。

验证(第二个提交 c31f10d7)

  • bun x tsc --noEmit → exit 0
  • bun test src/update/check.test.ts → 33 pass / 0 fail(108 expect,全部注入 mock fetch,不打真网络)
  • 全量 bun test → 978 pass / 1 fail;唯一失败是既有的 Windows-only captcha worker dispatch (worker / in-process) > an unloadable worker entry degrades to in-process solving,master 上同样失败
  • 真网络 smoke(当前版本 4.7.5,最新 release v4.7.5 走的是「已是新版」分支,所以用 checkForUpdate("4.7.4", ...) 触发提示文案):宿主机 → re-download from .../tag/v4.7.5;container=podman → podman compose pull && podman compose up -d;container=lxc → pull the new image and recreate the container

README / README_EN 里对应的段落也同步改了(容器提示与检测到的运行时有关)。

@TriDefender
TriDefender merged commit 1c84460 into TriDefender:master Oct 2, 2026
1 check passed
@TriDefender

Copy link
Copy Markdown
Owner

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants